﻿// Copyright © Naked Objects Group Ltd ( http://www.nakedobjects.net). 
// All Rights Reserved. This code released under the terms of the 
// Microsoft Public License (MS-PL) ( http://opensource.org/licenses/ms-pl.html) 

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Security.Principal;
using System.Web.Http;
using Common.Logging;
using NakedObjects.Surface;
using RestfulObjects.Snapshot.Constants;
using RestfulObjects.Snapshot.Representations;

namespace RestfulObjects.Snapshot.Utility {
    public class RestSnapshot {
        private readonly IList<string> allowHeaders = new List<string>();
        private readonly ILog logger = LogManager.GetCurrentClassLogger();
        private readonly Action populator;
        private readonly HttpRequestMessage requestMessage;
        private readonly IList<WarningHeaderValue> warningHeaders = new List<WarningHeaderValue>();

        private HttpStatusCode httpStatusCode = HttpStatusCode.OK;
        private Representation representation;

        static RestSnapshot() {
            AcceptHeaderStrict = true;
        }

        private RestSnapshot(HttpRequestMessage req) {
            requestMessage = req;
            ValidateIncomingMediaType();
        }

        private RestSnapshot(ContextSurface context, HttpRequestMessage req) : this(req) {
            logger.DebugFormat("RestSnapshot:{0}", context.GetType().FullName);
            CheckForRedirection(context, req);
        }

        public RestSnapshot(ObjectContextSurface objectContext, HttpRequestMessage req, RestControlFlags flags, HttpStatusCode httpStatusCode = HttpStatusCode.OK)
            : this(objectContext, req) {
            populator = () => {
                this.httpStatusCode = httpStatusCode;
                representation = ObjectRepresentation.Create(objectContext, req, flags);
                SetHeaders();
            };
        }

        public RestSnapshot(ActionResultContextSurface actionResultContext, HttpRequestMessage req, RestControlFlags flags)
            : this(actionResultContext, req) {
            populator = () => {
                representation = ActionResultRepresentation.Create(req, actionResultContext, flags);
                SetHeaders();
            };
        }

        public RestSnapshot(ObjectContextSurface[] objectContext, HttpRequestMessage req, RestControlFlags flags)
            : this(req) {
            logger.DebugFormat("RestSnapshot:ObjectList");
            populator = () => {
                representation = ListRepresentation.Create(objectContext, req, flags);
                SetHeaders();
            };
        }

        public RestSnapshot(PropertyContextSurface propertyContext, HttpRequestMessage req, RestControlFlags flags)
            : this(propertyContext, req) {
            FilterBlobsAndClobs(propertyContext, flags);
            populator = () => {
                representation = MemberRepresentation.Create(req, propertyContext, flags);
                SetHeaders();
            };
        }

        public RestSnapshot(PropertyTypeContextSurface propertyTypeContext, HttpRequestMessage req, RestControlFlags flags)
            : this(req) {
            logger.DebugFormat("RestSnapshot:{0}", propertyTypeContext.GetType().FullName);
            populator = () => {
                representation = MemberTypeRepresentation.Create(req, propertyTypeContext, flags);
                SetHeaders();
            };
        }

        public RestSnapshot(ActionContextSurface actionContext, HttpRequestMessage req, RestControlFlags flags)
            : this(actionContext, req) {
            populator = () => {
                representation = ActionRepresentation.Create(req, actionContext, flags);
                SetHeaders();
            };
        }

        public RestSnapshot(ActionTypeContextSurface actionTypeContext, HttpRequestMessage req, RestControlFlags flags)
            : this(req) {
            logger.DebugFormat("RestSnapshot:{0}", actionTypeContext.GetType().FullName);

            populator = () => {
                representation = ActionTypeRepresentation.Create(req, actionTypeContext, flags);
                SetHeaders();
            };
        }

        public RestSnapshot(ParameterTypeContextSurface parameterTypeContext, HttpRequestMessage req, RestControlFlags flags)
            : this(req) {
            logger.DebugFormat("RestSnapshot:{0}", parameterTypeContext.GetType().FullName);

            populator = () => {
                representation = ParameterTypeRepresentation.Create(req, parameterTypeContext, flags);
                SetHeaders();
            };
        }

        public RestSnapshot(HttpRequestMessage req, RestControlFlags flags)
            : this(req) {
            logger.DebugFormat("RestSnapshot:Home");


            populator = () => {
                representation = HomePageRepresentation.Create(req, flags);
                SetHeaders();
            };
        }

        public RestSnapshot(IDictionary<string, string> capabilities, HttpRequestMessage req, RestControlFlags flags)
            : this(req) {
            logger.DebugFormat("RestSnapshot:Version");


            populator = () => {
                representation = VersionRepresentation.Create(req, capabilities, flags);
                SetHeaders();
            };
        }

        public RestSnapshot(IPrincipal user, HttpRequestMessage req, RestControlFlags flags)
            : this(req) {
            logger.DebugFormat("RestSnapshot:User");


            populator = () => {
                representation = UserRepresentation.Create(req, user, flags);
                SetHeaders();
            };
        }

        public RestSnapshot(INakedObjectSpecificationSurface[] specs, HttpRequestMessage req, RestControlFlags flags)
            : this(req) {
            logger.DebugFormat("RestSnapshot:TypeList");


            populator = () => {
                representation = ListRepresentation.Create(specs, req, flags);
                SetHeaders();
            };
        }

        public RestSnapshot(INakedObjectSpecificationSurface spec, HttpRequestMessage req, RestControlFlags flags)
            : this(req) {
            logger.DebugFormat("RestSnapshot:DomainType");


            populator = () => {
                representation = DomainTypeRepresentation.Create(req, spec, flags);
                SetHeaders();
            };
        }

        public RestSnapshot(TypeActionInvokeContext typeActionInvokeContext, HttpRequestMessage req, RestControlFlags flags)
            : this(req) {
            logger.DebugFormat("RestSnapshot:{0}", typeActionInvokeContext.GetType().FullName);

            populator = () => {
                representation = TypeActionInvokeRepresentation.Create(req, typeActionInvokeContext, flags);
                SetHeaders();
            };
        }

        public RestSnapshot(Exception exception, HttpRequestMessage req)
            : this(req) {
            logger.DebugFormat("RestSnapshot:Exception");

            populator = () => {
                MapToHttpError(exception);
                MapToRepresentation(exception, req);
                MapToWarningHeader(exception);
                SetHeaders();
            };
        }

        public static bool AcceptHeaderStrict { get; set; }

        public Representation Representation {
            get { return representation; }
        }

        public CacheType Caching { get; set; }

        public MediaTypeHeaderValue ContentType { get; set; }

        public Uri Location { get; set; }

        public WarningHeaderValue[] WarningHeaders {
            get { return warningHeaders.ToArray(); }
        }

        public string[] AllowHeaders {
            get { return allowHeaders.ToArray(); }
        }

        public HttpStatusCode HttpStatusCode {
            get { return httpStatusCode; }
            private set { httpStatusCode = value; }
        }

        public EntityTagHeaderValue Etag { get; set; }

        private static void CheckForRedirection(ContextSurface context, HttpRequestMessage req) {
            var ocs = context as ObjectContextSurface;
            var arcs = context as ActionResultContextSurface;
            Tuple<string, string> redirected = (ocs != null ? ocs.Redirected : null) ?? (arcs != null && arcs.Result != null ? arcs.Result.Redirected : null);

            if (redirected != null) {
                Uri redirectAddress = new UriMtHelper(req).GetRedirectUri(req, redirected.Item1, redirected.Item2);
                throw new HttpResponseException(new HttpResponseMessage(HttpStatusCode.MovedPermanently) {Headers = {Location = redirectAddress}});
            }
        }

        private static void FilterBlobsAndClobs(PropertyContextSurface propertyContext, RestControlFlags flags) {
            if (!flags.BlobsClobs) {
                PredefinedType? pdt = RestUtils.SpecToPredefinedType(propertyContext.Specification);

                if (pdt == PredefinedType.Blob || pdt == PredefinedType.Clob) {
                    throw new PropertyResourceNotFoundNOSException(propertyContext.Id);
                }
            }
        }

        public RestSnapshot Populate() {
            try {
                populator();
                return this;
            }
            catch (HttpResponseException e) {
                logger.DebugFormat("HttpResponse exception being passed up {0}", e.Message);
                throw;
            }
            catch (Exception e) {
                throw new GeneralErrorNOSException(e);
            }
        }

        private static bool IsSupportedMediaType(string mediaType) {
            return mediaType == "*/*" ||
                   mediaType == "application/*" ||
                   mediaType == "application/json";
        }


        public void ValidateIncomingMediaType() {
            if (AcceptHeaderStrict) {
                HttpHeaderValueCollection<MediaTypeWithQualityHeaderValue> incomingMediaTypes = requestMessage.Headers.Accept;

                if (!incomingMediaTypes.Any() || incomingMediaTypes.Any(mt => IsSupportedMediaType(mt.MediaType))) {
                    return;
                }

                throw new HttpResponseException(new HttpResponseMessage(HttpStatusCode.NotAcceptable));
            }
        }

        public void ValidateOutgoingMediaType() {
            if (AcceptHeaderStrict) {
                List<NameValueHeaderValue> incomingParameters = requestMessage.Headers.Accept.SelectMany(a => a.Parameters).ToList();

                string[] incomingProfiles = incomingParameters.Where(nv => nv.Name == "profile").Select(nv => nv.Value).Distinct().ToArray();
                string[] outgoingProfiles = ContentType != null ? ContentType.Parameters.Where(nv => nv.Name == "profile").Select(nv => nv.Value).Distinct().ToArray() : new string[] {};

                if (incomingProfiles.Any() && outgoingProfiles.Any() && !outgoingProfiles.Intersect(incomingProfiles).Any()) {
                    if (outgoingProfiles.Contains(UriMtHelper.GetJsonMediaType(RepresentationTypes.Error).Parameters.First().Value)) {
                        // outgoing error so even though incoming profiles does not include error representation return error anyway but with 406 code
                        HttpStatusCode = HttpStatusCode.NotAcceptable;
                    }
                    else {
                        // outgoing profile not included in incoming profiles and not already an error so throw a 406
                        throw new HttpResponseException(new HttpResponseMessage(HttpStatusCode.NotAcceptable));
                    }
                }
            }
        }

        private void MapToRepresentation(Exception e, HttpRequestMessage req) {
            if (e is WithContextNOSException) {
                ArgumentsRepresentation.Format format = e is BadPersistArgumentsException ? ArgumentsRepresentation.Format.Full : ArgumentsRepresentation.Format.MembersOnly;
                RestControlFlags flags = e is BadPersistArgumentsException ? ((BadPersistArgumentsException) e).Flags : RestControlFlags.DefaultFlags();

                var contextNosException = e as WithContextNOSException;

                if (contextNosException.Contexts.Any(c => c.ErrorCause == Cause.Disabled || c.ErrorCause == Cause.Immutable)) {
                    representation = null;
                }
                else if (contextNosException.ContextSurface != null) {
                    representation = ArgumentsRepresentation.Create(req, contextNosException.ContextSurface, format, flags);
                }
                else if (contextNosException.Contexts.Any()) {
                    representation = ArgumentsRepresentation.Create(req, contextNosException.Contexts, format, flags);
                }
                else {
                    representation = null;
                }
                ContentType = UriMtHelper.GetJsonMediaType(RepresentationTypes.Error);
            }
            else if (e is ResourceNotFoundNOSException          ||
                     e is NotAllowedNOSException                ||    
                     e is PreconditionFailedNOSException        ||
                     e is PreconditionHeaderMissingNOSException ||
                     e is NoContentNOSException) {
                representation = null;
            }
            else {
                representation = ErrorRepresentation.Create(e);
            }
        }

        private void SetHeaders() {
            if (representation != null) {
                ContentType = representation.GetContentType();
                Etag = representation.GetEtag();
                Caching = representation.GetCaching();

                foreach (string w in representation.GetWarnings()) {
                    warningHeaders.Add(new WarningHeaderValue(299, "RestfulObjects", "\"" + w + "\""));
                }

                if (httpStatusCode == HttpStatusCode.Created) {
                    Location = representation.GetLocation();
                }
            }
        }

        private void MapToHttpError(Exception e) {
            const HttpStatusCode unprocessableEntity = (HttpStatusCode) 422;
            const HttpStatusCode preconditionHeaderMissing = (HttpStatusCode) 428;

            if (e is ResourceNotFoundNOSException) {
                httpStatusCode = HttpStatusCode.NotFound;
            }
            else if (e is BadArgumentsNOSException) {
                var bre = e as BadArgumentsNOSException;

                if (bre.Contexts.Any(c => c.ErrorCause == Cause.Immutable)) {
                    httpStatusCode = HttpStatusCode.MethodNotAllowed;
                }
                else if (bre.Contexts.Any(c => c.ErrorCause == Cause.Disabled)) {
                    httpStatusCode = HttpStatusCode.Forbidden;
                }
                else {
                    httpStatusCode = unprocessableEntity;
                }
            }
            else if (e is BadRequestNOSException) {
                httpStatusCode = HttpStatusCode.BadRequest;
            }
            else if (e is NotAllowedNOSException) {
                httpStatusCode = HttpStatusCode.MethodNotAllowed;
            }
            else if (e is NoContentNOSException) {
                httpStatusCode = HttpStatusCode.NoContent;
            }
            else if (e is PreconditionFailedNOSException) {
                httpStatusCode = HttpStatusCode.PreconditionFailed;
            }
            else if (e is PreconditionHeaderMissingNOSException) {
                httpStatusCode = preconditionHeaderMissing;
            }
            else {
                httpStatusCode = HttpStatusCode.InternalServerError;
            }
        }

        private void MapToWarningHeader(Exception e) {
            IList<string> warnings = new List<string>();

            if (e is ResourceNotFoundNOSException) {
                warnings.Add(e.Message);
            }
            else if (e is WithContextNOSException) {
                var bae = e as WithContextNOSException;

                if (bae.Contexts.Any(c => c.ErrorCause == Cause.Immutable)) {
                    warnings.Add("object is immutable");
                    allowHeaders.Add("GET");
                }
                else if (bae.Contexts.Any()) {
                    foreach (string w in bae.Contexts.Where(c => !String.IsNullOrEmpty(c.Reason)).Select(c => c.Reason)) {
                        warnings.Add(w);
                    }
                }
                else {
                    warnings.Add(bae.Message);
                }
            }
            else if (e is NoContentNOSException) {
                // do nothing 
            }
            else {
                warnings.Add(e.Message);
            }

            foreach (string w in warnings) {
                try {
                    // remove all \" within warning message as they cause format exception 
                    warningHeaders.Add(new WarningHeaderValue(199, "RestfulObjects", "\"" + w.Replace('"', ' ') + "\""));
                }
                catch (FormatException fe) {
                    logger.WarnFormat("Failed to parse warning message: {0} : {1}", w, fe.Message);
                    warningHeaders.Add(new WarningHeaderValue(199, "RestfulObjects", "\"" + "Failed to parse warning message" + "\""));
                }
            }
        }
    }
}